Quarto Dashboards

for Monitoring Data Collection

Yebelay · C4ED Ethiopia

2026-05-12

Session Overview

01
What is Quarto / R Markdown?
From dynamic documents to the .qmd ecosystem
05
Value Boxes
KPI numbers: syntax, icons, colors, dynamic values
02
Quarto Dashboard
Minimal YAML, rendering, and the dashboard structure
06
Tabsets & Sidebars
Multiple views, filter panels, global vs. page-level
03
Pages, Rows & Columns
Building layout with # ## ### headings and size control
07
04
Cards
Code-chunk outputs, manual cards, scrollable rows

What is Quarto / R Markdown?

  • R Markdown combines narrative text (markdown) with executable code chunks (usually R) into a single dynamic document.

  • Quarto is the next-generation R Markdown from Posit, same idea, more features, multi-language.

    • Multi-language: R, Python, Julia, Observable
    • Many outputs: HTML, PDF, Word, slides, websites, dashboards
    • Reproducible: code + narrative in one .qmd
    • Compatible: most .Rmd files render unchanged

Live example

My personal site is built with Quarto: https://yebelay.rbind.io/

Quarto vs R Markdown

If you know R Markdown, you already know most of Quarto. Here’s what changed:

Aspect R Markdown Quarto
File format .Rmd .qmd
Engine knitr only knitr + Jupyter
Languages Mainly R R, Python, Julia, Observable
Output formats Via rmarkdown package Built-in, unified YAML
YAML option output: html_document format: dashboard
Chunk options ```{r echo=FALSE} #| echo: false (inline)
Dashboards flexdashboard package format: dashboard (built-in)

Tip

Think of Quarto as one tool for everything you currently do across rmarkdown, bookdown, xaringan, and flexdashboard.

What is a Quarto Dashboard?

A single .qmd file that renders into a fully interactive web dashboard (no Shiny server required for static content).

📁 One file

Structure, code, and text live together in a single .qmd.

🚀 No server

Renders to a standalone .html you can email or host.

🔬 Multiple languages

Use the right engine: R, Python, Julia, or Observable.

Tip

A dashboard is just a .qmd file with format: dashboard in the YAML.

Step 1 - Minimal .qmd File

💻 Code

dashboard.qmd
---
title: "LLRP I Dashboard"
format: dashboard
---

```{r}
#| title: "Scatter Plot"
plot(mtcars$hp, mtcars$mpg)
```

```{r}
#| title: "Summary Table"
knitr::kable(head(mtcars, 5))
```

🔄 What happens when you click Render

  1. Quarto reads your .qmd file
  2. knitr (or Jupyter) runs your code chunks
  3. Each chunk output → becomes a card
  4. Dashboard .html is saved alongside the .qmd
  5. The browser opens automatically to show the result

Tip

Save the file as dashboard.qmd, then click Render (Ctrl+Shift+K in RStudio).

YAML Options Reference

_quarto.yml
---
title: "C4ED Programme Monitor"
author: "Yebelay"
format:
  dashboard:
    theme: flatly      # bootswatch theme
    orientation: rows  # rows (default)
    logo: logo.png     # nav bar logo
    scrolling: true    # allow scrolling
    expandable: true   # fullscreen cards
execute:
  echo: false          # hide all code
  warning: false
  message: false
---
Option What it does
theme: Built-in Bootswatch: flatly, cosmo, minty, darkly
orientation: rows (default) → ## creates rows · columns## creates columns
scrolling: true allows the page to scroll; false pins card heights
echo: false Always set this - hides all R/Python code from output
expandable: Adds a fullscreen button to every card automatically

Important

Set echo: false globally in execute: - never per-chunk - so code is never accidentally exposed to stakeholders.

The Heading Hierarchy

Top bar with the icon, title, author, and links to each # page.

🔲 Layout

Pages, rows, columns, and tabsets - all defined by Markdown headings (#, ##, ###).

🃏 Containers

Cards display output. Sidebars hold filters. Toolbars add inline inputs.

  • Dashboard layout is entirely controlled by Markdown heading levels. Never skip a level.

Important

The rule: ###### → chunk. Skipping a level breaks the layout.

Heading Hierarchy - Reference Table

Level Syntax Creates Key attribute
1 # Page name Navigation tab in top bar background-color=
2 ## Row name Horizontal row band {height=N%} · {.tabset} · {.sidebar}
3 ### Col name Column inside a row {width=N%}
```{r} chunk One card inside a column #| title: · #| fig-height:
content: valuebox KPI value box #| icon: · #| color:

Tip

The hierarchy is always # → ## → ### → chunk.

Pages: Level-1 Headings #

pages.qmd
---
title: "LLRP I Dashboard"
format: dashboard
---
# Overview
# Follow-up
# Daily
# Teams
# Woredas
# Spousal
# RCE

Key points

  • Each # = one navigation tab at the top
  • Readers click tabs to switch between pages

- Add {background-color="#047B77"} for colored section dividers

Rows: Level-2 Headings ##

rows.qmd
# Overview
## Key figures {height=20%}
content here

## Main charts {height=55%}
content here

## Data table {height=25%}
content here

Rules

  • ## creates a full-width horizontal band
  • Content inside a row flows side by side
  • Use {height=N%} to control the vertical split
  • Rows are stacked top to bottom on the page

Note

Heights should sum to ~100%. Quarto distributes leftover space automatically if they don’t.

Columns: Level-3 Headings ###

columns.qmd
# Overview

## Row 1 {height=28%}

### Column A {width=30%}
content here

### Column B {width=40%}
content here

### Column C {width=30%}
content here

Heading Default (orientation: rows) With orientation: columns
## Horizontal row Vertical column
### Column inside row Row inside column
{height=N%} On ## row On ### row
{width=N%} On ### column On ## column

What is a Card? Code → Output

Every code chunk output is automatically wrapped in a card. The #| title: option sets the card header.

card.qmd
```{r}
#| title: "Beneficiaries by team"
#| fig-height: 4
#| expandable: true
ggplot(df, aes(x = team, y = reach, fill = team)) +
  geom_col(show.legend = FALSE) +
  theme_c4ed()
```

::: {.card title="Key finding"}
Write any **markdown** here.

- Bullet one
- Bullet two
:::

Use a manual .card div for text-only cards (key findings, notes).

Tip

#| expandable: true adds a fullscreen button to any card.

Value Box: Syntax

valuebox.qmd
## KPI row {height=20%}
```{r vbox1}
#| content: valuebox
#| title: "Beneficiaries"
#| icon: people-fill
#| color: "#047B77"
list(value = scales::comma(14823))
```
```{r vbox2}
#| content: valuebox
#| title: "Woredas"
#| color: "#1A9490"
list(value = 16)
```

Dynamic value

list(value = scales::comma(
  sum(df$beneficiaries)))

Color options

#| color: primary | success | warning | danger
#| color: "#047B77"   # teal
#| color: "#F7941D"   # orange

Trend indicator

change <- round((cur-prev)/prev*100, 1)
list(value = paste0("↑ ", change, "%"))

Tip

Find icon names at https://icons.getbootstrap.com - search any concept and copy the name (e.g. graph-up, geo-alt-fill, people-fill).

Overview

page1_overview.qmd
# Overview
## KPIs {height=20%}
```{r}
#| content: valuebox
#| title: "Beneficiaries"
#| icon: people-fill
#| color: "#047B77"
list(value = scales::comma(sum(df$reach)))
```
```{r}
#| content: valuebox
#| title: "Woredas"
#| color: "#1A9490"
list(value = n_distinct(df$woreda))
```
## Charts {height=78% .tabset}
### By team
```{r}
#| title: "Reach by team"
ggplot(df, aes(team, reach, fill = team)) +
  geom_col() + theme_c4ed()
```

Note

Result: three KPI value boxes on top, a tabbed chart row below. Same skeleton works for every monitoring dashboard.

Tabsets: Multiple Views, One Row

tabset.qmd
## Analysis {.tabset}      # ← key
### By team                # tab 1
```{r}
plot_by_team()
```
### By month               # tab 2
```{r}
plot_by_month()
```
### Raw data               # tab 3
```{r}
DT::datatable(my_data)
```

Rules

  • Add {.tabset} to a ## Row heading to activate
  • ### sub-headings become individual tab labels
  • Works for charts, tables, text - any card content
  • Combine with value boxes: KPIs on top, tabs below

Tip

Use tabsets when you have 3+ views of the same data. For 2 views, side-by-side columns often look cleaner.

Tabsets - Live Demo

The same data, three lenses - click the tabs:

Reach by team (demo data)
team reach
A 412
B 538
C 297
D 614
E 480

Full Dashboard Structure

dashboard.qmd
---
title: "C4ED Programme Dashboard"
format:
  dashboard:
    theme: flatly
    orientation: rows
    scrolling: true
    logo: img/c4ed_logo.png
execute:
  echo: false
  warning: false
---
```{r setup, include=FALSE}
library(ggplot2); library(dplyr)
library(DT);      library(leaflet)
df <- readr::read_csv("data/field_data.csv")
```
# Overview      # value boxes + charts
# Daily         # trend + map + table
# Data          # interactive DT tables
# KPI           # summary stats

Build in this order

  1. Write YAML header
  2. Create a setup chunk
  3. Add # page headings
  4. Add ## rows (with {height=N%})
  5. Add ### columns (with {width=N%})
  6. Add code chunks
  7. Render and iterate (Ctrl+Shift+K)

Workflow Tips

  1. Start with structure only - add all # ## ### headings with placeholder text. Render to check layout before any code.
  2. One chunk = one card - never put two ggplot() calls in one chunk if you want separate cards.
  3. echo: false globally - set in YAML execute: block once. Never accidentally expose code to stakeholders.
  4. Name every chunk - {r valuebox-reach}, {r chart-country} make debugging much faster.
  5. Test heights iteratively - row heights that look good locally may shift on a projector. Aim for values that sum to ~95%.

Common Errors

Error Fix
Blank white page Check YAML indentation
Cards overlapping Reduce {height=N%} values
Code visible in output Add echo: false in execute:
Empty value box Return must be a list()
Icon not showing Check spelling on icons.getbootstrap.com
“skipped heading level” warning Don’t jump from # to ### - go through ##

Quick Reference Card

quick_ref.qmd
# Structure (Markdown headings)
# Page              # navigation tab
## Row              # horizontal band   {height=N%}
### Column          # column in row     {width=N%}

# Row/column modifiers
{.tabset}           # tabbed row
{.sidebar}          # left sidebar panel
{.scrollable}       # allow row to scroll
{.fill}             # stretch to fill row

# Code chunk options
#| content: valuebox    # make a value box
#| title: "label"       # card header title
#| icon: people         # Bootstrap icon name
#| color: "#047B77"     # background color
#| fig-height: 4        # plot height (inches)
#| fig-width: 8         # plot width
#| padding: 0px         # remove card padding
#| expandable: true     # fullscreen button

Resources

Tip

Add server: shiny to YAML → full Shiny app. Then use input$ and output$ as normal.

Example

Live demo: https://yebelay.github.io/llrp-dashboard/

Thank you!

Yebelay Berehan. C4ED Ethiopia ·
yebelay.berehan@c4ed.org